package com.googlecode.cswish.util;

import java.beans.PropertyDescriptor;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.annotation.Resource;

import ognl.OgnlException;
import ognl.OgnlRuntime;

import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.springframework.stereotype.Service;

import com.googlecode.cswish.struts.spring.GenericService;
import com.googlecode.cswish.struts.spring.ObjectFactory;
import com.opensymphony.xwork2.conversion.impl.XWorkConverter;
import com.opensymphony.xwork2.inject.Inject;
import com.opensymphony.xwork2.ognl.OgnlUtil;

/**
 * Support [] operation -- update and get a group of values
 * 	 
 * @author jerry.feng Date: 2008-11-7
 */
@Service
public class OgnlExUtil {
	
	private final static Logger logger = Logger.getLogger(OgnlExUtil.class);
	
	// For Spring JUnit test
	private OgnlUtil ognlUtil = new OgnlUtil();

	private XWorkConverter xworkConverter;
	
	@Resource
	private ObjectFactory objectFactory;
	
	private GenericService genericService;
	
	@Inject
	public void setOgnlUtil(OgnlUtil ognlUtil) {
		this.ognlUtil = ognlUtil;
	}
	
	@Inject
    public void setXWorkConverter(XWorkConverter xworkConverter) {
        this.xworkConverter = xworkConverter;
    }
	
	private GenericService getGenericService() {
		if (genericService == null) {
			genericService = objectFactory.getBean(GenericService.class, false);
		}
		return genericService;
	}
	
	public OgnlUtil getOgnlUtil() {
		if (ognlUtil == null) {
			objectFactory.getContainer().inject(this);
		}
		return this.ognlUtil;
	}
	
	public XWorkConverter getXWorkConverter() {
		if (xworkConverter == null) {
			objectFactory.getContainer().inject(this);
		}
		return this.xworkConverter;
	}
	
	public boolean isAggregate(String name) {
		return name.indexOf("[]", 0) > 0;
	}
	
	public <T> T convertValue(Object value, Class<T> toType) {
		return convertValue(value, toType, null);
	}
	
	public <T> T convertValue(Object value, Class<T> toType, String property) {
		T val = (T)getXWorkConverter().convertValue(null, null, null, property, value, toType);
		if (OgnlRuntime.NoConversionPossible.equals(val)) {
			val = null;
		}
		return val;
	}
	
	public List<Object> getAggregateValue(String name, Map context, Object root) {
		try {
			List<Object> aggregate = new ArrayList<Object>();
			addToAggregate(aggregate, name, context, root);
			return aggregate;
		} catch (OgnlException ex) {
			logger.error(ex);
		}
		return null;
	}

	private void addToAggregate(List<Object> aggregate, String name,
			Map context, Object root) throws OgnlException {
		int next = name.indexOf("[]");
		
		// it's standard name
		if (next == -1) {
			Object value = ognlUtil.getValue(name, context, root);
			aggregate.add(value);
		} else {
			// found '[]'
			String prefix = name.substring(0, next);
			Object values = ognlUtil.getValue(prefix, context, root);
			
			// it has values
			if (values != null) {
				boolean isLastElement = (next + 3 >= name.length());
				if (!isLastElement) {
					// skip "[]."
					name = name.substring(next + 3);
				}
				
				boolean isArray = values.getClass().isArray();
				if (isArray) {
					Object[] arrays = ((Object[])values);
					if (isLastElement) {
						for (Object obj : arrays) {
							aggregate.add(obj);
						}
					} else {
						for (Object obj : arrays) {
							addToAggregate(aggregate, name, context, obj);
						}
					}
				} else if (values instanceof Collection) {
					Collection<Object> coll = (Collection<Object>) values;
					if (isLastElement) {
						aggregate.addAll(coll);
					} else {
						for (Object obj : coll) {
							addToAggregate(aggregate, name, context, obj);
						}
					}
				} else {
					logger.error("Wrong operation:" + name + " at position " + next);
				}
			}
		}
	}

	
	public void setEachAggregateValue(String name, Map context, Object root, Collection values) {
		// 1. analyze the name
		String[] names = StringUtils.split(name, "[]");
		int index = 0;
		for (Object value : values) {
			// TODO: indexs.length should equals names.length - 1
			String nestedName = getNestedName(names, new int[]{index++});
			setAggregateValue(nestedName, context, root, value);
		}
	}
	
	// a[].b[].c ==> {a,.b,.c} {1,2} ==> a[1].b[2].c 
	private String getNestedName(String[] names, int[] indexs) {
		StringBuilder sb = new StringBuilder();
		int lastIndex = names.length - 1;
		for (int i = 0; i < names.length; i++) {
			String name = names[i];
			sb.append(name);
			if (i < lastIndex) {
				sb.append('[').append(indexs[i]).append(']');
			}
		}
		return sb.toString();
	}
	
	/**
	 * i.g. setAggregateValue("a.b[].id", context, root, 1)
	 * <p>
	 * each id in array 'b' is updated to '1'
	 * 
	 * @param name
	 * @param context
	 * @param root
	 * @param value
	 */
	public void setAggregateValue(String name, Map context, Object root, Object value) {
		try {
			int next = name.indexOf("[]");
			// it's standard name
			if (next == -1) {
				ognlUtil.setValue(name, context, root, value);
			} else {
				// found '[]'
				String prefix = name.substring(0, next);
				Object values = ognlUtil.getValue(prefix, context, root);
				
				// it has values
				if (values != null) {
					boolean isLastElement = (next + 3 >= name.length());
					if (!isLastElement) {
						// skip "[]."
						name = name.substring(next + 3);
					}
					
					boolean isArray = values.getClass().isArray();
					if (isArray) {
						Object[] arrays = ((Object[])values);
						
						if (isLastElement) {
							// don't update the last array, it's meaningless to store repeated value to array 
						} else {
							for (Object obj : arrays) {
								setAggregateValue(name, context, obj, value);
							}
						}
					} else if (values instanceof Collection) {
						Collection<Object> coll = (Collection<Object>) values;
						if (isLastElement) {
							// don't update the last array, it's meaningless to store repeated value to array
						} else {
							for (Object obj : coll) {
								setAggregateValue(name, context, obj, value);
							}
						}
					} else {
						logger.error("Wrong operation:" + name + " at position " + next);
					}
				}
			}
		} catch (OgnlException ex) {
			logger.error(ex);
		}
	}

	public Object getValue(String name, Map context, Object root) {
		try {
			return ognlUtil.getValue(name, context, root);
		} catch (OgnlException ex) {
			logger.debug(ex);
		}
		return null;
	}
	
	public void setValue(String name, Map context, Object root, Object value) {
		try {
			ognlUtil.setValue(name, context, root, value);
		} catch (OgnlException ex) {
			logger.debug("Failed to set value", ex);
		}
	}
	
	public <T> T clone(T model) {
		T to = (T) objectFactory.buildModel(model.getClass().getName(), BlankObject.Map);
		ognlUtil.copy(model, to, BlankObject.Map);
		return to;
	}
	
	/**
	 * Update value from 'from' to 'to'
	 * 
	 * Notice: from.class = to.class & from != to && from.id == to.id
	 * 
	 * @param from
	 * @param to
	 * @param inputField 
	 */
	public void deepUpdate(Object from, Object to, Set<String> inputField) {
		deepUpdate(from, to, null, inputField, new IdentityHashMap());
	}
	
	private void deepUpdate(Object from, Object to, String namePrefix, Set<String> inputField, Map identityMap) {
		if (identityMap.get(from) != null || from == to) {
			return ;
		}
		identityMap.put(from, from);
		
		try {
			PropertyDescriptor[] descriptors = ognlUtil.getPropertyDescriptors(from);
			
			for (PropertyDescriptor descriptor : descriptors) {
				if (descriptor.getReadMethod() != null && descriptor.getWriteMethod() != null && !"id".equals(descriptor.getName())) {
					Object[] NULL = null;
					Object fromValue = descriptor.getReadMethod().invoke(from, NULL);
					String fieldName = namePrefix == null ? descriptor.getName() : namePrefix + '.' + descriptor.getName();
					if (fromValue != null) {
						Class<?> type = fromValue.getClass();
						// 1. update the simple object
						if (ClassUtil.isSimpleObj(type)){
							descriptor.getWriteMethod().invoke(to, fromValue);
							
						// 2. update list object
						} else if (ClassUtil.isCollection(type)){
							// 2.1 populate the list information
							Collection<Object> destColl = (Collection<Object>) descriptor.getReadMethod().invoke(to, NULL);
							
							Collection<Object> fromColl = (Collection<Object>) fromValue;
							List<Object>[] destInfo = getInfo(destColl);
							List<Object> destIds = destInfo[0];
							List<Object> destModels = destInfo[1];
							List<Object>[] fromInfo = getInfo(fromColl);
							List<Object> fromIds = fromInfo[0];
							List<Object> fromModels = fromInfo[1];
							
							// 2.1 remove the missed object
							for (int i = 0, len = destIds.size(); i < len; i++) {
								Object destId = destIds.get(i);
								
								int index = fromIds.indexOf(destId);
								if (index < 0) {
									destColl.remove(destModels.get(i));
								}
							}
							
							
							// 2.2 compare the dest list according the id information
							List<Object> updateList = new ArrayList<Object>();
							if (destColl == null) {
								destColl = new ArrayList<Object>();
								descriptor.getWriteMethod().invoke(to, destColl);
							}
							for (int i = 0, len = fromIds.size(); i < len; i++) {
								Object fromId = fromIds.get(i);
								Object fromModel = fromModels.get(i);
								// 2.2.1 add the new model object
								if (fromId == null) {
									destColl.add(fromModel);
								} else {
									int index = destIds.indexOf(fromId);
									// 2.2.2 update the existed model
									if (index >= 0) {
										// from
										updateList.add(fromModel);
										// to
										updateList.add(destModels.get(index));
									
										// 2.2.3 remove the missed model
									} else {
										// show warning?! found the new wrong model id
										destColl.add(fromModel);
									}
								}
							}
							
							// update child model
							for (int i = 0, len = updateList.size(); i < len; i++,i++) {
								deepUpdate(updateList.get(i), updateList.get(i+1), fieldName, inputField, identityMap);
							}
						} else {
							// 3. update model object
							Object destValue = descriptor.getReadMethod().invoke(to, NULL);
							if (destValue == null) {
								descriptor.getWriteMethod().invoke(to, fromValue);
							} else {
								Map context = new HashMap();
								Serializable fromId = getId(fromValue, context);
								Serializable destId = getId(destValue, context);
								if (destId == null || destId != null && !destId.equals(fromId)) {// the dest model is changed
									if (fromId != null) {
										// try to find the existed model from db
										Object existed = getGenericService().load(fromValue);
										if (existed != null) {
											destValue = existed;
											destId = fromId;
										}
									}
									// no matched model, the dest model is itself (input model)
									if (destId != fromId) {
										destValue = fromValue;
									}
									descriptor.getWriteMethod().invoke(to, destValue);
								}
								
								// update the child model property only when the matched model is found
								if (destId == null && fromId == null || destId.equals(fromId)) {
									deepUpdate(fromValue, destValue, fieldName, inputField, identityMap);
								}
							} // update model object
						}
					} else {	// the input value is null
						if (inputField != null && inputField.contains(fieldName)) {	// it's a input field
							if (ClassUtil.isCollection(descriptor.getReadMethod().getReturnType())) {
								Collection<?> coll = (Collection<?>) descriptor.getReadMethod().invoke(to, null);
								if (coll != null) {
									coll.clear();
								}
							} else {
								descriptor.getWriteMethod().invoke(to, new Object[]{null});	// set the field to null
							}
						}
					}
				}
			}
		} catch (Exception ex) {
			logger.error("deepUpdate", ex);
		}
	}
	
	private Serializable getId(Object model, Map context) {
		try {
			return (Serializable) ognlUtil.getValue("id", context, model);
		} catch (OgnlException e) {
			return null;
		}
	}
	
	private List<Object>[] getInfo(Collection<Object> coll) {
		List<Serializable> ids = new ArrayList<Serializable>();
		List<Object> objects = new ArrayList<Object>();
		Map context = new HashMap();
		if (coll != null) {
			for (Object model : coll) {
				ids.add(getId(model, context));
				objects.add(model);
			}
		}
		return new List[] {ids, objects};
	}
}
